Skip to content

feat: Add permission_ids support to team member profiles and invitations#870

Closed
bootssecurity wants to merge 14 commits intohexclave:devfrom
bootssecurity:dev
Closed

feat: Add permission_ids support to team member profiles and invitations#870
bootssecurity wants to merge 14 commits intohexclave:devfrom
bootssecurity:dev

Conversation

@bootssecurity
Copy link
Copy Markdown

@bootssecurity bootssecurity commented Aug 31, 2025

  • Add permission_ids field to team member profile schemas (client/server)
  • Update team member profiles API to include permission_ids in responses
  • Modify team member list UI to display roles based on permission_ids
  • Update team invitation endpoints to handle permission_ids
  • Add role-permissions endpoint for fetching available permissions
  • Update documentation with permission_ids examples and type definitions
  • Add conceptual documentation for role-based team invitations

Important

Add permission_ids support to team member profiles and invitations, enhancing role-based access control.

  • Behavior:
    • Add permission_ids to team member profiles and invitations in team-member-profiles/crud.ts and team-invitation/crud.ts.
    • Update API endpoints in server-interface.ts and client-interface.ts to handle permission_ids.
    • Add role-permissions endpoint in role-permissions/route.tsx to fetch available permissions.
  • UI:
    • Modify team-member-invitation-section.tsx and team-member-list-section.tsx to display roles based on permission_ids.
    • Add role selection in invitation forms.
  • Documentation:
    • Update teams-management.mdx and role-based-team-invitations.mdx with permission_ids examples and type definitions.
  • Misc:
    • Add getTeamRolePermissions() to client-app-impl.ts and server-app-impl.ts for fetching role permissions.
    • Update team-profile.mdx and team.mdx to include permission_ids.

This description was created by Ellipsis for cf8e76b. You can customize this summary. It will automatically update as commits are pushed.

Summary by CodeRabbit

  • New Features

    • Role-based team invitations: invite form supports selecting role/permission IDs; invitations and member profiles expose permissionIds; new endpoint and client API to list available role permissions.
  • Behavioral

    • Permission grants on acceptance are applied transactionally/atomically with membership creation.
  • Documentation

    • Guides and SDK/type docs updated for role-based invitations and usage.
  • Tests

    • Expanded end-to-end tests covering flows, edge cases, and permission validation.

- Add permission_ids field to team member profile schemas (client/server)
- Update team member profiles API to include permission_ids in responses
- Modify team member list UI to display roles based on permission_ids
- Update team invitation endpoints to handle permission_ids
- Add role-permissions endpoint for fetching available permissions
- Update documentation with permission_ids examples and type definitions
- Add conceptual documentation for role-based team invitations
@vercel
Copy link
Copy Markdown

vercel Bot commented Aug 31, 2025

@bootssecurity is attempting to deploy a commit to the Stack Team on Vercel.

A member of the Team first needs to authorize it.

@CLAassistant
Copy link
Copy Markdown

CLAassistant commented Aug 31, 2025

CLA assistant check
All committers have signed the CLA.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Aug 31, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds role-based permission support for team invitations: invitation payloads can include permission_ids, surfaced in CRUD and member profiles, selectable in the UI, exposed via a new role-permissions endpoint, and granted transactionally when an invitation is accepted. Tests and docs updated.

Changes

Cohort / File(s) Summary
Invitation acceptance
apps/backend/src/app/api/latest/team-invitations/accept/verification-code-handler.tsx
Membership creation moved into retryTransaction(prisma, ...); deduplicates and grants permission_ids inside the same transaction using grantTeamPermission(tx, ...); schema updated to accept permission_ids; imports added.
Invitation send & CRUD
apps/backend/src/app/api/latest/team-invitations/send-code/route.tsx, apps/backend/src/app/api/latest/team-invitations/crud.tsx
send-code POST body accepts optional permission_ids and forwards them; CRUD list/read include permission_ids (fallback []).
Role-permissions endpoint
apps/backend/src/app/api/latest/team-invitations/role-permissions/route.tsx
New GET route returning team-scoped permission definitions as { items: [{ id, description?, contained_permission_ids[] }], is_paginated: false }.
Team member profiles (backend)
apps/backend/src/app/api/latest/team-member-profiles/crud.tsx
Adds fetchTeamMemberPermissions batched loader; prismaToCrud signature extended to accept permissionIds; list/read/update outputs include permission_ids to avoid N+1 queries.
Shared interfaces & schemas
packages/stack-shared/src/interface/client-interface.ts, .../server-interface.ts, .../crud/team-invitation.ts, .../crud/team-member-profiles.ts
Client sendTeamInvitation accepts optional permissionIds; new getTeamRolePermissions method added; server sendServerTeamInvitation accepts optional permissionIds; CRUD schemas add permission_ids.
SDK types & implementations
packages/template/src/lib/stack-app/.../client-app-impl.ts, .../server-app-impl.ts, packages/template/src/lib/stack-app/teams/index.ts
Propagates permission IDs through Team/TeamUser/TeamInvitation/TeamProfile types (snake_case ↔ camelCase mapping); inviteUser signatures accept optional permissionIds; client adds getTeamRolePermissions().
Frontend UI — invitations & members
packages/template/src/components-page/account-settings/teams/team-member-invitation-section.tsx, .../team-member-list-section.tsx
Invitation form adds role selector populated from role-permissions endpoint; invitations and member rows render Role badges derived from permissionIds (filters internal IDs starting with $, maps known IDs to friendly labels); form reset/submit behavior updated.
Docs & examples
docs/concepts/role-based-team-invitations.mdx, docs/templates-python/.../teams-management.mdx, docs/templates/sdk/types/team-profile.mdx, docs/templates/sdk/types/team.mdx
New guide and examples for role-based invitations; Python SDK examples extended with permission_ids and get_team_role_permissions; SDK/type docs updated to include permissionIds.
E2E tests
apps/e2e/tests/backend/endpoints/api/v1/team-member-profiles.test.ts, .../team-invitations.test.ts
Tests added/updated to assert permission_ids in profiles and invitations, role-permissions endpoint behavior, transactional application of permissions on accept, and multiple edge/error cases (invalid IDs, 400/403/401 scenarios).

Sequence Diagram(s)

sequenceDiagram
    participant Admin as Admin (inviter)
    participant UI as Frontend
    participant API as Backend API
    participant DB as Database

    rect rgb(220,245,220)
    Note over Admin,API: Create & send invitation with role
    Admin->>UI: select role + email
    UI->>API: POST /team-invitations/send-code { email, permission_ids: [...] }
    API->>DB: persist invitation with permission_ids
    API-->>UI: 200 Invitation created
    end

    rect rgb(220,230,255)
    Note over User,API: Recipient accepts invitation
    User->>API: POST /team-invitations/accept { code }
    API->>API: retryTransaction(start)
    API->>DB: create membership (tx)
    API->>DB: grant permissions for permission_ids (tx)
    API->>API: retryTransaction(commit)
    API-->>User: 200 Joined team
    end

    rect rgb(255,245,210)
    Note over UI,API: Display members & roles
    UI->>API: GET /team-member-profiles
    API->>DB: fetch members + permission_ids (batched)
    API-->>UI: members[] with permission_ids
    UI->>UI: map permission_ids -> role label
    UI-->>Admin: show member with Role badge
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

  • Areas to focus review on:
    • Transaction boundaries and error propagation in verification-code-handler.tsx.
    • Correctness and batching logic in fetchTeamMemberPermissions and mapping to listed members.
    • Schema and naming consistency across snake_case (server) and camelCase (client) surfaces.
    • Frontend role selection, default behavior, and form reset semantics.
    • E2E tests for flakiness around permission application and transactional commits.

Possibly related PRs

  • Custom item customers #855 — Modifies the same invitation acceptance handler; likely related due to overlapping changes to the accept flow and transaction handling.
  • Workflows #873 — Changes retryTransaction signature/behavior; relevant because this diff calls retryTransaction(prisma, ...) for transactional permission grants.

Poem

🐰 I nibbled through schemas, carrots in a row,
Invitations now carry roles that gently glow.
Grants wrapped in one small hop — all done as one,
Members wear their badges, shining in the sun.
A happy twitch, a grateful hop — the team’s work done.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the main change: adding permission_ids support to team member profiles and invitations.
Description check ✅ Passed The description is well-structured with clear bullet points covering behavior, UI, documentation, and miscellaneous changes. It includes detailed context about the feature implementation.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7d71fc3 and 708fe14.

📒 Files selected for processing (1)
  • apps/e2e/tests/backend/endpoints/api/v1/team-invitations.test.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
apps/e2e/tests/backend/endpoints/api/v1/team-invitations.test.ts (3)
apps/e2e/tests/backend/backend-helpers.ts (3)
  • niceBackendFetch (109-173)
  • createMailbox (59-66)
  • backendContext (35-57)
packages/template/src/lib/stack-app/projects/index.ts (1)
  • Project (10-15)
packages/template/src/lib/stack-app/teams/index.ts (1)
  • Team (38-52)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Vercel Agent Review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts Outdated
Comment thread packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts Outdated
@recurseml
Copy link
Copy Markdown

recurseml Bot commented Aug 31, 2025

Review by RecurseML

🔍 Review performed on 6a3459e..cf8e76b

Severity Location Issue
Medium packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts:656 Inconsistent naming convention using snake_case instead of camelCase
Medium packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts:1438 Inconsistent naming convention using snake_case instead of camelCase in return type
✅ Files analyzed, no issues (4)

packages/template/src/components-page/account-settings/teams/team-member-invitation-section.tsx
packages/template/src/components-page/account-settings/teams/team-member-list-section.tsx
apps/backend/src/app/api/latest/team-invitations/role-permissions/route.tsx
apps/backend/src/app/api/latest/team-member-profiles/crud.tsx

⏭️ Files skipped (low suspicion) (14)

apps/backend/src/app/api/latest/team-invitations/accept/verification-code-handler.tsx
apps/backend/src/app/api/latest/team-invitations/crud.tsx
apps/backend/src/app/api/latest/team-invitations/send-code/route.tsx
docs/concepts/role-based-team-invitations.mdx
docs/templates-python/concepts/teams-management.mdx
docs/templates/sdk/types/team-profile.mdx
docs/templates/sdk/types/team.mdx
packages/stack-shared/src/interface/client-interface.ts
packages/stack-shared/src/interface/crud/team-invitation.ts
packages/stack-shared/src/interface/crud/team-member-profiles.ts
packages/stack-shared/src/interface/server-interface.ts
packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts
packages/template/src/lib/stack-app/apps/interfaces/client-app.ts
packages/template/src/lib/stack-app/teams/index.ts

Need help? Join our Discord

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Greptile Summary

This PR implements role-based team invitations and permission management for Stack Auth, extending the existing team management system with granular permission control. The changes add permission_ids support across the entire stack - from database schemas and API endpoints to client interfaces and UI components.

Key architectural changes include:

  • Schema Extensions: Added permission_ids field to team member profiles and team invitations in both client/server CRUD schemas, enabling storage and retrieval of permission arrays
  • API Enhancements: Modified team invitation endpoints (/send-code, /accept) to handle optional permission_ids parameter, and added a new /role-permissions endpoint to fetch available team permissions
  • Client Interface Updates: Extended team invitation methods (sendTeamInvitation, inviteUser) to accept optional permissionIds parameter and added getTeamRolePermissions() method for fetching available permissions
  • Backend Logic: Updated invitation acceptance handler to grant specific permissions when permission_ids are provided, in addition to default team permissions
  • UI Components: Enhanced team member list to display roles as badges based on permission_ids, and updated invitation form with role selection dropdown
  • Data Layer: Modified team member profile CRUD operations to fetch and return permission data by querying the TeamMemberDirectPermission table

The implementation maintains backward compatibility by making permission_ids optional throughout, allowing existing functionality to continue working while enabling enhanced permission management. The changes follow established patterns in the codebase for CRUD operations, API design, and client-server communication. Comprehensive documentation has been added covering the new role-based invitation concepts, API usage, and migration guidance.

Confidence score: 2/5

  • This PR introduces complex permission logic with several implementation inconsistencies that could cause production issues
  • Score lowered due to schema conflicts, API documentation mismatches, and potential null safety issues in UI components
  • Pay close attention to packages/stack-shared/src/interface/crud/team-member-profiles.ts for duplicate field definitions and apps/backend/src/app/api/latest/team-invitations/role-permissions/route.tsx for behavior/documentation mismatch

19 files reviewed, 14 comments

Edit Code Review Bot Settings | Greptile

Comment thread packages/template/src/lib/stack-app/teams/index.ts Outdated
Comment thread packages/template/src/lib/stack-app/apps/interfaces/client-app.ts Outdated
Comment thread apps/backend/src/app/api/latest/team-member-profiles/crud.tsx Outdated
Comment thread apps/backend/src/app/api/latest/team-member-profiles/crud.tsx Outdated
Comment thread packages/stack-shared/src/interface/crud/team-member-profiles.ts Outdated
Comment thread docs/concepts/role-based-team-invitations.mdx
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
packages/stack-shared/src/interface/crud/team-member-profiles.ts (2)

31-33: Docs grammar: fix “your are”

“that your are a member” → “that you are a member”.

-      description: "List team members profiles. You always need to specify a `team_id` that your are a member of on the client. You can always filter for your own profile by setting `me` as the `user_id` in the path parameters. If you want list all the profiles in a team, you need to have the `$read_members` permission in that team.",
+      description: "List team members profiles. You always need to specify a `team_id` that you are a member of on the client. You can filter for your own profile by setting `me` as the `user_id` in the path parameters. If you want to list all the profiles in a team, you need the `$read_members` permission in that team.",

41-43: Docs grammar/casing

Start sentence with capital “You”.

-      description: "Get a team member profile. you can always get your own profile by setting `me` as the `user_id` in the path parameters on the client. If you want to get someone else's profile in a team, you need to have the `$read_members` permission in that team.",
+      description: "Get a team member profile. You can always get your own profile by setting `me` as the `user_id` in the path parameters on the client. If you want to get someone else's profile in a team, you need the `$read_members` permission in that team.",
apps/backend/src/app/api/latest/team-invitations/send-code/route.tsx (1)

35-50: Privilege escalation risk: inviter can assign arbitrary permissions

There’s no authorization check that the inviter is allowed to grant the requested permission_ids. A user with only $invite_members could escalate invitees to admin by including powerful permissions. Ensure each requested permission is within the inviter’s effective permissions (or gate behind a stronger “assign” capability).

   await retryTransaction(prisma, async (tx) => {
     if (auth.type === "client") {
       if (!auth.user) throw new KnownErrors.UserAuthenticationRequired();

       await ensureUserTeamPermissionExists(tx, {
         tenancy: auth.tenancy,
         userId: auth.user.id,
         teamId: body.team_id,
         permissionId: "$invite_members",
         errorType: 'required',
         recursive: true,
       });
+
+      // Optional: require an explicit assignment capability instead of subset check
+      // await ensureUserTeamPermissionExists(tx, { ...permissionId: "$assign_permissions", errorType: 'required', recursive: true });
+
+      // Ensure inviter can grant every requested permission
+      const grantIds = Array.from(new Set(body.permission_ids ?? []));
+      for (const pid of grantIds) {
+        await ensureUserTeamPermissionExists(tx, {
+          tenancy: auth.tenancy,
+          userId: auth.user.id,
+          teamId: body.team_id,
+          permissionId: pid,
+          errorType: 'forbidden',
+          recursive: true,
+        });
+      }
     }
   });
🧹 Nitpick comments (19)
packages/stack-shared/src/interface/server-interface.ts (1)

662-671: Nit: omit undefined in request body and keep header casing consistent

Use filterUndefined to avoid serializing undefined, and match header casing used elsewhere.

Apply:

       {
         method: "POST",
         headers: {
-          "Content-Type": "application/json"
+          "content-type": "application/json"
         },
-        body: JSON.stringify({
-          email: options.email,
-          team_id: options.teamId,
-          callback_url: options.callbackUrl,
-          permission_ids: options.permissionIds,
-        }),
+        body: JSON.stringify(filterUndefined({
+          email: options.email,
+          team_id: options.teamId,
+          callback_url: options.callbackUrl,
+          permission_ids: options.permissionIds,
+        })),
       },
docs/templates/sdk/types/team-profile.mdx (1)

54-66: Clarify whether permissionIds are direct vs. effective permissions and link to role-permissions

Add a brief note indicating if these are direct grants only (not inherited) and link to the role-permissions endpoint doc for mapping IDs to roles.

docs/templates/sdk/types/team.mdx (2)

207-213: Document the new permissionIds option in Parameters

Add a ParamField entry so readers see the option outside the signature.

Apply:

           <ParamField path="callbackUrl" type="string">
             The URL where users will be redirected after accepting the team invitation.
             
             Required when calling `inviteUser()` in the server environment since the URL cannot be automatically determined.
             
             Example: `https://your-app-url.com/handler/team-invitation`
           </ParamField>
+          <ParamField path="permissionIds" type="string[]">
+            Optional list of permission IDs to grant upon acceptance. Omit to use the project's default role.
+          </ParamField>

324-326: Keep "Returns" text consistent with signature (include permissionIds)

The Signature includes permissionIds, but the "Returns" text above still omits it.

Apply:

-      `Promise<{ id: string, email: string, expiresAt: Date }[]>`
+      `Promise<{ id: string, email: string, expiresAt: Date, permissionIds: string[] }[]>`
apps/backend/src/app/api/latest/team-invitations/crud.tsx (1)

56-57: Prefer nullish coalescing over logical OR for defaulting

Use ?? to default only when undefined/null.

Apply:

-          permission_ids: code.data.permission_ids || [],
+          permission_ids: code.data.permission_ids ?? [],
packages/template/src/lib/stack-app/apps/interfaces/client-app.ts (1)

57-57: Ensure return shape aligns with backend payload

Confirm the backend route returns contained_permission_ids and is_paginated: false exactly as typed here, and consider promoting this response shape to a shared exported type in stack-shared to avoid drift.

Apply this diff if you want to centralize the type:

-    getTeamRolePermissions(): Promise<{ items: { id: string, description?: string, contained_permission_ids: string[] }[], is_paginated: false }>,
+    getTeamRolePermissions(): Promise<RolePermissionsList>,

(With RolePermissionsList introduced under stack-shared and imported here.)

packages/template/src/components-page/account-settings/teams/team-member-list-section.tsx (1)

24-52: Localize role labels and avoid raw IDs in UI

  • Wrap “Admin”/“Member” with t(...).
  • Avoid showing raw role IDs; prefer mapping via getTeamRolePermissions() (id → human label), falling back to t("Member").
-  const userRoles = useMemo(() => {
+  const userRoles = useMemo(() => {
     const rolesMap = new Map<string, string>();
-
     for (const user of users) {
       // Use permission_ids directly from teamProfile
       const permissionIds = user.teamProfile.permission_ids || [];
       // Filter out $-prefixed permissions
       const filteredPermissions = permissionIds.filter((id: string) => !id.startsWith('$'));
       // Find matching role based to permission IDs
-      let roleName = "Member";
+      let roleName = t("Member");
       if (filteredPermissions.length > 0) {
         const roleId = filteredPermissions[0];
         if (roleId === 'team_admin') {
-          roleName = "Admin";
+          roleName = t("Admin");
         } else if (roleId === 'team_member') {
-          roleName = "Member";
+          roleName = t("Member");
         } else {
-          roleName = roleId;
+          roleName = roleId; // TODO: map via getTeamRolePermissions() description/name if available
         }
       }
       rolesMap.set(user.id, roleName);
     }
     return rolesMap;
-  }, [users]);
+  }, [users, t]);

If you want, I can wire in getTeamRolePermissions() to replace the raw ID fallback.

apps/backend/src/app/api/latest/team-invitations/send-code/route.tsx (1)

52-57: Normalize permission_ids before embedding into the code

Dedup to reduce redundant work downstream and keep payload minimal.

-    const codeObj = await teamInvitationCodeHandler.sendCode({
+    const codeObj = await teamInvitationCodeHandler.sendCode({
       tenancy: auth.tenancy,
       data: {
         team_id: body.team_id,
-        permission_ids: body.permission_ids || [],
+        permission_ids: Array.from(new Set(body.permission_ids ?? [])),
       },
apps/backend/src/app/api/latest/team-member-profiles/crud.tsx (3)

82-92: Eliminate N+1 permission queries.

This does one findMany per member. Batch once and group by (teamId, projectUserId) to cut roundtrips and improve latency.

Example replacement:

-      const permissions = await Promise.all(db.map(async (member) => {
-        const perms = await tx.teamMemberDirectPermission.findMany({
-          where: {
-            tenancyId: auth.tenancy.id,
-            projectUserId: member.projectUserId,
-            teamId: member.teamId,
-          },
-          select: { permissionId: true },
-        });
-        return perms.map(p => p.permissionId);
-      }));
+      const keys = db.map(m => `${m.teamId}:${m.projectUserId}`);
+      const permsAll = await tx.teamMemberDirectPermission.findMany({
+        where: {
+          tenancyId: auth.tenancy.id,
+          projectUserId: { in: db.map(m => m.projectUserId) },
+          teamId: { in: db.map(m => m.teamId) },
+        },
+        select: { permissionId: true, teamId: true, projectUserId: true },
+      });
+      const permsMap = new Map<string, string[]>();
+      for (const p of permsAll) {
+        const k = `${p.teamId}:${p.projectUserId}`;
+        (permsMap.get(k) ?? permsMap.set(k, []).get(k)!).push(p.permissionId);
+      }
+      const permissions = keys.map(k => permsMap.get(k) ?? []);

94-99: Consider using user signup time as fallback, not membership createdAt.

getUsersLastActiveAtMillis expects a per-user signup/time fallback. Passing teamMember.createdAt may skew “last active” for older users who joined a team later. Prefer projectUser.createdAt.

-      const lastActiveAtMillis = await getUsersLastActiveAtMillis(auth.project.id, auth.branchId, db.map(user => user.projectUserId), db.map(user => user.createdAt));
+      const lastActiveAtMillis = await getUsersLastActiveAtMillis(
+        auth.project.id,
+        auth.branchId,
+        db.map(u => u.projectUserId),
+        db.map(u => u.projectUser.createdAt),
+      );

21-23: Optional: stabilize permission_ids ordering.

Sort permission_ids for deterministic output to reduce cache churn and flakey UI diffs.

-    permission_ids: permissionIds,
+    permission_ids: [...permissionIds].sort(),
docs/concepts/role-based-team-invitations.mdx (1)

7-12: Minor grammar/polish.

  • “ability to:” → keep, but ensure each bullet is parallel (verb form).
  • Consider “granular access control by letting you define exactly which permissions an invited user should have upon joining.”
packages/template/src/components-page/account-settings/teams/team-member-invitation-section.tsx (3)

39-45: Reduce console noise and localize labels; minor cleanup.

  • Drop dev console logs.
  • Localize “Admin”/“Member” strings via t().

Apply:

-        console.log('Fetching role permissions...');
         const response = await stackApp.getTeamRolePermissions();
-        console.log('Role permissions fetched:', response.items);
         setRolePermissions(response.items);
@@
-      console.log('No permissionIds provided, returning default');
       return t("Default member role");
@@
-      if (roleId === 'team_admin') return 'Admin';
-      if (roleId === 'team_member') return 'Member';
+      if (roleId === 'team_admin') return t('Admin');
+      if (roleId === 'team_member') return t('Member');
@@
-    if (firstPermissionId === 'team_admin') return 'Admin';
-    if (firstPermissionId === 'team_member') return 'Member';
+    if (firstPermissionId === 'team_admin') return t('Admin');
+    if (firstPermissionId === 'team_member') return t('Member');
@@
-                      {permission.id === 'team_admin' ? 'Admin' :
-                        permission.id === 'team_member' ? 'Member' :
+                      {permission.id === 'team_admin' ? t('Admin') :
+                        permission.id === 'team_member' ? t('Member') :
                         permission.id}

Also applies to: 55-57, 216-219, 76-79, 83-86


66-71: Micro-optimization: use Set for membership checks instead of a Map-to-self.

Current roleMap maps id->id, then you .get to test presence. A Set is clearer.

Apply:

-    // Map permission IDs to their IDs (instead of descriptions)
-    const roleMap = new Map(rolePermissions.map(role => [role.id, role.id]));
+    // Fast membership check of known role IDs
+    const roleIds = new Set(rolePermissions.map(role => role.id));
@@
-    const matchingRoles = filteredPermissionIds.map(id => roleMap.get(id)).filter(Boolean);
+    const matchingRoles = filteredPermissionIds.filter(id => roleIds.has(id));

35-49: Duplicate fetching of role permissions; consider a shared hook or prop-drilling.

Both components fetch the same data separately. Extract a useTeamRolePermissions() hook (with memoized cache) or fetch once in the parent and pass down.

I can sketch a small useTeamRolePermissions() hook backed by a simple in-memory cache to avoid duplicate network calls. Want me to add it?

Also applies to: 153-165

packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts (3)

656-657: Defensive default for permission_ids.

If older backends omit permission_ids, this will be undefined and violate TeamMemberProfile’s string[] type. Default to [].

Apply:

-        permission_ids: crud.permission_ids,
+        permission_ids: crud.permission_ids ?? [],

666-667: Match invitation shape defensively.

Same rationale as above for invitations.

Apply:

-      permissionIds: crud.permission_ids,
+      permissionIds: crud.permission_ids ?? [],

1438-1457: Consider lightweight validation/caching for role-permissions; drop unnecessary header.

  • GET doesn’t need Content-Type.
  • Optional: add a small cache to avoid refetching per component mount; or validate the expected shape to fail fast.

Apply minimal cleanup:

-      {
-        method: "GET",
-        headers: {
-          "Content-Type": "application/json",
-        },
-      },
+      { method: "GET" },

If you want, I can add a createCacheBySession for role-permissions and wire a useAsyncCache variant.

docs/templates-python/concepts/teams-management.mdx (1)

220-241: Docs align well; small clarity tweaks suggested.

  • In send_team_invitation, explicitly note that omitting permission_ids (or passing None/empty) applies the default member role.
  • In examples, mention that the valid IDs come from get_team_role_permissions to avoid guesswork.

Apply:

@@
-    Send an invitation to join a team with optional role-based permissions
+    Send an invitation to join a team with optional role-based permissions.
+    If permission_ids is None or empty, the default member role is applied.
@@
-# Example usage - Send invitation with admin role
+# Example usage - Send invitation with admin role (IDs from get_team_role_permissions)

Also applies to: 248-267, 298-323, 331-348, 456-488

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 6a3459e and cf8e76b.

📒 Files selected for processing (19)
  • apps/backend/src/app/api/latest/team-invitations/accept/verification-code-handler.tsx (3 hunks)
  • apps/backend/src/app/api/latest/team-invitations/crud.tsx (1 hunks)
  • apps/backend/src/app/api/latest/team-invitations/role-permissions/route.tsx (1 hunks)
  • apps/backend/src/app/api/latest/team-invitations/send-code/route.tsx (3 hunks)
  • apps/backend/src/app/api/latest/team-member-profiles/crud.tsx (4 hunks)
  • docs/concepts/role-based-team-invitations.mdx (1 hunks)
  • docs/templates-python/concepts/teams-management.mdx (8 hunks)
  • docs/templates/sdk/types/team-profile.mdx (2 hunks)
  • docs/templates/sdk/types/team.mdx (4 hunks)
  • packages/stack-shared/src/interface/client-interface.ts (2 hunks)
  • packages/stack-shared/src/interface/crud/team-invitation.ts (1 hunks)
  • packages/stack-shared/src/interface/crud/team-member-profiles.ts (1 hunks)
  • packages/stack-shared/src/interface/server-interface.ts (2 hunks)
  • packages/template/src/components-page/account-settings/teams/team-member-invitation-section.tsx (4 hunks)
  • packages/template/src/components-page/account-settings/teams/team-member-list-section.tsx (3 hunks)
  • packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts (5 hunks)
  • packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts (4 hunks)
  • packages/template/src/lib/stack-app/apps/interfaces/client-app.ts (1 hunks)
  • packages/template/src/lib/stack-app/teams/index.ts (4 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Prefer ES6 Map over Record where feasible

Files:

  • packages/template/src/lib/stack-app/apps/interfaces/client-app.ts
  • packages/stack-shared/src/interface/crud/team-invitation.ts
  • apps/backend/src/app/api/latest/team-invitations/crud.tsx
  • packages/stack-shared/src/interface/crud/team-member-profiles.ts
  • apps/backend/src/app/api/latest/team-invitations/role-permissions/route.tsx
  • apps/backend/src/app/api/latest/team-invitations/send-code/route.tsx
  • packages/template/src/components-page/account-settings/teams/team-member-list-section.tsx
  • packages/stack-shared/src/interface/server-interface.ts
  • packages/template/src/components-page/account-settings/teams/team-member-invitation-section.tsx
  • packages/stack-shared/src/interface/client-interface.ts
  • apps/backend/src/app/api/latest/team-member-profiles/crud.tsx
  • apps/backend/src/app/api/latest/team-invitations/accept/verification-code-handler.tsx
  • packages/template/src/lib/stack-app/teams/index.ts
  • packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts
  • packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
apps/backend/src/app/api/latest/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place backend API route handlers under /apps/backend/src/app/api/latest and follow RESTful, resource-based paths (auth, users, teams, oauth-providers)

Files:

  • apps/backend/src/app/api/latest/team-invitations/crud.tsx
  • apps/backend/src/app/api/latest/team-invitations/role-permissions/route.tsx
  • apps/backend/src/app/api/latest/team-invitations/send-code/route.tsx
  • apps/backend/src/app/api/latest/team-member-profiles/crud.tsx
  • apps/backend/src/app/api/latest/team-invitations/accept/verification-code-handler.tsx
🧠 Learnings (1)
📚 Learning: 2025-08-24T18:36:37.712Z
Learnt from: CR
PR: stack-auth/stack-auth#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-24T18:36:37.712Z
Learning: Applies to apps/backend/src/app/api/latest/**/*.ts : Use the custom route handler system in the backend for consistent API responses

Applied to files:

  • apps/backend/src/app/api/latest/team-invitations/role-permissions/route.tsx
🧬 Code graph analysis (9)
packages/stack-shared/src/interface/crud/team-member-profiles.ts (1)
packages/stack-shared/src/interface/crud/users.ts (1)
  • usersCrudServerReadSchema (24-57)
apps/backend/src/app/api/latest/team-invitations/role-permissions/route.tsx (3)
apps/backend/src/route-handlers/smart-route-handler.tsx (1)
  • createSmartRouteHandler (209-294)
packages/stack-shared/src/schema-fields.ts (7)
  • yupObject (247-251)
  • clientOrHigherAuthTypeSchema (481-481)
  • adaptSchema (330-330)
  • yupNumber (191-194)
  • yupString (187-190)
  • yupArray (213-216)
  • yupBoolean (195-198)
apps/backend/src/lib/permissions.tsx (1)
  • listPermissionDefinitions (183-193)
apps/backend/src/app/api/latest/team-invitations/send-code/route.tsx (1)
packages/stack-shared/src/schema-fields.ts (2)
  • yupArray (213-216)
  • permissionDefinitionIdSchema (680-689)
packages/template/src/components-page/account-settings/teams/team-member-invitation-section.tsx (2)
packages/template/src/lib/translations.tsx (1)
  • useTranslation (4-19)
packages/stack-shared/src/schema-fields.ts (3)
  • yupObject (247-251)
  • strictEmailSchema (448-448)
  • yupString (187-190)
packages/stack-shared/src/interface/client-interface.ts (1)
packages/stack-shared/src/sessions.ts (1)
  • InternalSession (51-212)
apps/backend/src/app/api/latest/team-member-profiles/crud.tsx (1)
apps/backend/src/app/api/latest/users/crud.tsx (2)
  • getUsersLastActiveAtMillis (199-223)
  • getUserLastActiveAtMillis (188-194)
apps/backend/src/app/api/latest/team-invitations/accept/verification-code-handler.tsx (2)
packages/stack-shared/src/schema-fields.ts (2)
  • yupArray (213-216)
  • permissionDefinitionIdSchema (680-689)
apps/backend/src/lib/permissions.tsx (1)
  • grantTeamPermission (96-138)
packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts (1)
packages/template/src/utils/url.ts (1)
  • constructRedirectUrl (4-20)
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts (1)
packages/template/src/utils/url.ts (1)
  • constructRedirectUrl (4-20)
🪛 LanguageTool
docs/concepts/role-based-team-invitations.mdx

[grammar] ~7-~7: There might be a mistake here.
Context: ...nvitation flow by adding the ability to: - Select specific role-based permissions d...

(QB_NEW_EN)


[grammar] ~162-~162: There might be a mistake here.
Context: ...e.g., $update_team, $invite_members) - Role-Based Permissions: Custom permiss...

(QB_NEW_EN)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Security Check
🔇 Additional comments (16)
packages/stack-shared/src/interface/crud/team-invitation.ts (1)

10-10: Addition of permission_ids to client read schema — LGTM

Shape aligns with backend list response; required array with empty fallback is appropriate.

packages/stack-shared/src/interface/server-interface.ts (1)

653-659: Public API: added optional permissionIds to sendServerTeamInvitation

Signature change looks good and matches docs/UI updates.

docs/templates/sdk/types/team-profile.mdx (1)

21-22: Add permissionIds to ToC — LGTM

docs/templates/sdk/types/team.mdx (1)

352-359: useInvitations return shape updated — LGTM

apps/backend/src/app/api/latest/team-invitations/accept/verification-code-handler.tsx (1)

110-129: Should invitations add permissions for existing members?

Currently, permissions are only applied when a new membership is created. If a user is already a member, the invitation silently skips grants. Confirm this matches product expectations.

Would you like a follow-up change to apply (or merge) permission_ids for existing members too?

packages/template/src/components-page/account-settings/teams/team-member-list-section.tsx (1)

71-92: LGTM: role column rendering

The new Role column and badge rendering are straightforward and readable.

apps/backend/src/app/api/latest/team-invitations/send-code/route.tsx (1)

24-25: Schema addition looks good

Accepting optional permission_ids with permissionDefinitionIdSchema matches shared constraints.

apps/backend/src/app/api/latest/team-member-profiles/crud.tsx (1)

15-24: Output shape extension looks good.

Adding permission_ids to prismaToCrud keeps naming consistent with API contracts.

packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts (3)

603-604: LGTM: surfaced permission_ids on teamProfile.

Keeps server model aligned with CRUD response.


613-617: LGTM: invitations expose permissionIds.

Matches shared interface semantics.


674-680: Verify server interface compatibility for permissionIds.

Confirm StackServerInterface.sendServerTeamInvitation({ permissionIds }) exists and is wired to backend send-code route expecting permission_ids.

Run a quick grep to confirm the method signature and payload mapping exist in server interface and transport layer.

packages/stack-shared/src/interface/client-interface.ts (1)

690-709: LGTM: sendTeamInvitation propagates permissionIds → permission_ids.

apps/backend/src/app/api/latest/team-invitations/role-permissions/route.tsx (1)

1-3: Good use of Smart Route Handler per backend guidelines.

docs/concepts/role-based-team-invitations.mdx (1)

35-68: Ensure examples match API output.

If backend filters out $permissions, remove those entries from the example payload; otherwise keep as-is.

packages/template/src/lib/stack-app/teams/index.ts (1)

13-14: All inviteUser call sites now use the object signature – every invocation passes an options object with email, optional callbackUrl, and optional permissionIds as required.

packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts (1)

733-741: LGTM: inviteUser forwards permissionIds correctly and refreshes cache.

No issues; consistent with the new public API.

Comment thread apps/backend/src/app/api/latest/team-member-profiles/crud.tsx Outdated
Comment thread docs/concepts/role-based-team-invitations.mdx
Comment thread packages/stack-shared/src/interface/client-interface.ts
Comment thread packages/stack-shared/src/interface/crud/team-member-profiles.ts
Comment thread packages/stack-shared/src/interface/crud/team-member-profiles.ts
@patched-codes
Copy link
Copy Markdown

patched-codes Bot commented Aug 31, 2025

Documentation Changes Required

Based on the recent pull request, the following documentation changes are needed:

1. docs/templates/sdk/types/team.mdx

  1. Update the inviteUser function documentation:

    • Add new permissionIds parameter in <ParamField> section:
      <ParamField path="permissionIds" type="string[]">
        The permission IDs to assign to the invited user. When not provided, the user will be assigned default member permissions.
      </ParamField>
      
    • Update function signature:
      declare function inviteUser(options: {
        email: string;
        callbackUrl?: string;
        permissionIds?: string[];
      }): Promise<void>;
    • Add example with role assignment:
      await team.inviteUser({
        email: 'user@example.com',
        permissionIds: ['team_admin'], // Invite as admin
      });
  2. Update return type signatures for listInvitations() and useInvitations() methods:

    • Change Promise<{ id: string, email: string, expiresAt: Date }[]> to Promise<{ id: string, email: string, expiresAt: Date, permissionIds?: string[] }[]>
    • Change { id: string, email: string, expiresAt: Date }[] to { id: string, email: string, expiresAt: Date, permissionIds?: string[] }[]

2. docs/templates/sdk/types/team-profile.mdx

  1. Add a new section to the TeamProfile table of contents for the permission_ids property.
  2. Add a new CollapsibleTypesSection for the permission_ids property, explaining:
    • It contains an array of permission IDs assigned to the user in the team
    • It can be used to determine the user's role in the team
    • Some special IDs like 'team_admin' and 'team_member' correspond to specific roles

3. docs/templates/sdk/objects/stack-app.mdx

  1. Add the getTeamRolePermissions method to the table of contents section:
    getTeamRolePermissions(): Promise<{...}>; //$stack-link-to:#stackclientappgetteamrolepermissions
    
  2. Add a new CollapsibleMethodSection for the getTeamRolePermissions method with appropriate description, parameters, return type, signature, and examples.

4. docs/templates-python/concepts/teams-management.mdx

Update the Python documentation for team invitations:

  1. Modify the send_team_invitation function (around line 217) to include the optional permission_ids parameter.
  2. Update the function signature and docstring.
  3. Update the JSON body in the function to include permission_ids when provided.
  4. Update the example usage to demonstrate how to invite a user with specific permissions.

Please ensure these changes are reflected in the relevant documentation files to accurately represent the new features and modifications introduced by the pull request.

…s API

- Add permission_ids field to team-member-profiles schema (client & server)
- Update CRUD operations to fetch and include direct permission_ids
- Fix team-member-list-section to use permission_ids for role display
- Optimize permission fetching to avoid N+1 queries
- Add atomic permission granting in team-invitations/accept
- Add e2e test coverage for permission_ids in API responses
- Fix naming convention violations (snake_case to camelCase)
- Remove debug console.log statements

Resolves broken member list roles functionality
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (6)
apps/backend/src/app/api/latest/team-invitations/accept/verification-code-handler.tsx (1)

100-131: Always grant permission_ids when accepting invites, even for existing members
Currently, permission grants are scoped to the !oldMembership branch, so re-invited members with new permission_ids receive no updates. Move the membership check inside the transaction and apply grants unconditionally to avoid this gap and eliminate the TOCTOU window:

@@ apps/backend/src/app/api/latest/team-invitations/accept/verification-code-handler.tsx
-    const oldMembership = await prisma.teamMember.findUnique({ … });
-
-    if (!oldMembership) {
-      await retryTransaction(prisma, async (tx) => {
-        await teamMembershipsCrudHandlers.adminCreate({ tenancy, team_id: data.team_id, user_id: user.id, data: {} });
-
-        if (data.permission_ids?.length) {
-          const uniquePermissionIds = [...new Set(data.permission_ids)];
-          for (const permissionId of uniquePermissionIds) {
-            await grantTeamPermission(tx, { tenancy, teamId: data.team_id, userId: user.id, permissionId });
-          }
-        }
-      });
-    }
+    await retryTransaction(prisma, async (tx) => {
+      const existing = await tx.teamMember.findUnique({
+        where: { tenancyId_projectUserId_teamId: { tenancyId: tenancy.id, projectUserId: user.id, teamId: data.team_id } },
+      });
+      if (!existing) {
+        await teamMembershipsCrudHandlers.adminCreate({ tenancy, team_id: data.team_id, user_id: user.id, data: {} });
+      }
+      if (data.permission_ids?.length) {
+        const uniquePermissionIds = Array.from(new Set(data.permission_ids));
+        for (const permissionId of uniquePermissionIds) {
+          await grantTeamPermission(tx, { tenancy, teamId: data.team_id, userId: user.id, permissionId });
+        }
+      }
+    });
apps/e2e/tests/backend/endpoints/api/v1/team-member-profiles.test.ts (5)

151-174: Update snapshot to include permission_ids.

API now returns permission_ids on member profiles.

@@
   expect(response2).toMatchInlineSnapshot(`
     NiceResponse {
       "status": 200,
       "body": {
         "is_paginated": false,
         "items": [
           {
             "display_name": "User 1",
+            "permission_ids": [],
             "profile_image_url": null,
             "team_id": "<stripped UUID>",
             "user_id": "<stripped UUID>",
           },
         ],
       },
       "headers": Headers { <some fields may have been hidden> },
     }
   `);

191-219: Avoid brittle list snapshot; assert structure including permission_ids.

Permissions vary by member. Replace the inline snapshot with structural checks.

-  expect(response3).toMatchInlineSnapshot(`
-    NiceResponse {
-      "status": 200,
-      "body": {
-        "is_paginated": false,
-        "items": [
-          {
-            "display_name": "User 3 (team creator)",
-            "profile_image_url": null,
-            "team_id": "<stripped UUID>",
-            "user_id": "<stripped UUID>",
-          },
-          {
-            "display_name": "User 1",
-            "profile_image_url": null,
-            "team_id": "<stripped UUID>",
-            "user_id": "<stripped UUID>",
-          },
-          {
-            "display_name": "User 2",
-            "profile_image_url": null,
-            "team_id": "<stripped UUID>",
-            "user_id": "<stripped UUID>",
-          },
-        ],
-      },
-      "headers": Headers { <some fields may have been hidden> },
-    }
-  `);
+  expect(response3.status).toBe(200);
+  expect(response3.body.is_paginated).toBe(false);
+  expect(Array.isArray(response3.body.items)).toBe(true);
+  for (const item of response3.body.items) {
+    expect(item).toEqual(
+      expect.objectContaining({
+        display_name: expect.any(String),
+        team_id: expect.any(String),
+        user_id: expect.any(String),
+        permission_ids: expect.any(Array),
+      })
+    );
+  }

229-240: Update update-profile snapshot to include permission_ids.

   expect(response4).toMatchInlineSnapshot(`
     NiceResponse {
       "status": 200,
       "body": {
         "display_name": "Team Member Name Updated",
+        "permission_ids": [],
         "profile_image_url": null,
         "team_id": "<stripped UUID>",
         "user_id": "<stripped UUID>",
       },
       "headers": Headers { <some fields may have been hidden> },
     }
   `);

251-266: Update read-self snapshot to include permission_ids.

   expect(response5).toMatchInlineSnapshot(`
     NiceResponse {
       "status": 200,
       "body": {
         "display_name": "Team Member Name Updated",
+        "permission_ids": [],
         "profile_image_url": null,
         "team_id": "<stripped UUID>",
         "user_id": "<stripped UUID>",
       },
       "headers": Headers { <some fields may have been hidden> },
     }
   `);

297-308: Avoid brittle read-other snapshot; assert structure including permission_ids.

User 2’s permissions may differ; prefer structural assertions.

-  expect(response7).toMatchInlineSnapshot(`
-    NiceResponse {
-      "status": 200,
-      "body": {
-        "display_name": "User 2 Updated",
-        "profile_image_url": null,
-        "team_id": "<stripped UUID>",
-        "user_id": "<stripped UUID>",
-      },
-      "headers": Headers { <some fields may have been hidden> },
-    }
-  `);
+  expect(response7.status).toBe(200);
+  expect(response7.body).toEqual(
+    expect.objectContaining({
+      display_name: "User 2 Updated",
+      team_id: expect.any(String),
+      user_id: expect.any(String),
+      permission_ids: expect.any(Array),
+    })
+  );
♻️ Duplicate comments (1)
apps/backend/src/app/api/latest/team-member-profiles/crud.tsx (1)

206-215: Fix wrong user id field in getUserLastActiveAtMillis call.

Use db.projectUserId, not db.projectUser.projectUserId.

-      return prismaToCrud(db, await getUserLastActiveAtMillis(auth.project.id, auth.branchId, db.projectUser.projectUserId) ?? db.projectUser.createdAt.getTime(), perms.map(p => p.permissionId));
+      return prismaToCrud(
+        db,
+        (await getUserLastActiveAtMillis(auth.project.id, auth.branchId, db.projectUserId)) ?? db.projectUser.createdAt.getTime(),
+        perms.map(p => p.permissionId)
+      );
🧹 Nitpick comments (3)
apps/backend/src/app/api/latest/team-invitations/accept/verification-code-handler.tsx (1)

33-35: Validate team_id as UUID for consistency.

Other routes use uuid() for team_id; align the schema here.

-  data: yupObject({
-    team_id: yupString().defined(),
+  data: yupObject({
+    team_id: yupString().uuid().defined(),
     permission_ids: yupArray(permissionDefinitionIdSchema.defined()).optional(),
   }).defined(),
apps/backend/src/app/api/latest/team-member-profiles/crud.tsx (2)

160-172: Align Read path with new helper keying.

Use the same team+user key to avoid future regressions.

-      const permissionMap = await fetchTeamMemberPermissions(
-        tx,
-        auth.tenancy.id,
-        db.teamId,
-        [db.projectUserId]
-      );
-
-      return prismaToCrud(
-        db,
-        await getUserLastActiveAtMillis(auth.project.id, auth.branchId, db.projectUserId) ?? db.projectUser.createdAt.getTime(),
-        permissionMap.get(db.projectUserId) || []
-      );
+      const permissionMap = await fetchTeamMemberPermissions(tx, auth.tenancy.id, [{ teamId: db.teamId, projectUserId: db.projectUserId }]);
+      const key = `${db.teamId}::${db.projectUserId}`;
+      return prismaToCrud(
+        db,
+        (await getUserLastActiveAtMillis(auth.project.id, auth.branchId, db.projectUserId)) ?? db.projectUser.createdAt.getTime(),
+        permissionMap.get(key) ?? []
+      );

206-213: DRY: reuse fetchTeamMemberPermissions in Update path.

Avoid duplicating permission queries.

-      const perms = await tx.teamMemberDirectPermission.findMany({
-        where: {
-          tenancyId: auth.tenancy.id,
-          projectUserId: db.projectUserId,
-          teamId: db.teamId,
-        },
-        select: { permissionId: true },
-      });
-
-      return prismaToCrud(db, await getUserLastActiveAtMillis(auth.project.id, auth.branchId, db.projectUser.projectUserId) ?? db.projectUser.createdAt.getTime(), perms.map(p => p.permissionId));
+      const pmap = await fetchTeamMemberPermissions(tx, auth.tenancy.id, [{ teamId: db.teamId, projectUserId: db.projectUserId }]);
+      const key = `${db.teamId}::${db.projectUserId}`;
+      return prismaToCrud(
+        db,
+        (await getUserLastActiveAtMillis(auth.project.id, auth.branchId, db.projectUserId)) ?? db.projectUser.createdAt.getTime(),
+        pmap.get(key) ?? []
+      );
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between cf8e76b and 1701d2d.

📒 Files selected for processing (8)
  • apps/backend/src/app/api/latest/team-invitations/accept/verification-code-handler.tsx (3 hunks)
  • apps/backend/src/app/api/latest/team-member-profiles/crud.tsx (4 hunks)
  • apps/e2e/tests/backend/endpoints/api/v1/team-member-profiles.test.ts (1 hunks)
  • packages/stack-shared/src/interface/crud/team-member-profiles.ts (1 hunks)
  • packages/template/src/components-page/account-settings/teams/team-member-invitation-section.tsx (4 hunks)
  • packages/template/src/components-page/account-settings/teams/team-member-list-section.tsx (3 hunks)
  • packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts (5 hunks)
  • packages/template/src/lib/stack-app/teams/index.ts (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • packages/stack-shared/src/interface/crud/team-member-profiles.ts
  • packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
  • packages/template/src/components-page/account-settings/teams/team-member-invitation-section.tsx
  • packages/template/src/components-page/account-settings/teams/team-member-list-section.tsx
  • packages/template/src/lib/stack-app/teams/index.ts
🧰 Additional context used
📓 Path-based instructions (4)
apps/e2e/**

📄 CodeRabbit inference engine (CLAUDE.md)

Always add new E2E tests when you change the API or SDK interface

Files:

  • apps/e2e/tests/backend/endpoints/api/v1/team-member-profiles.test.ts
**/*.test.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

In tests, prefer .toMatchInlineSnapshot where possible

Files:

  • apps/e2e/tests/backend/endpoints/api/v1/team-member-profiles.test.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Prefer ES6 Map over Record where feasible

Files:

  • apps/e2e/tests/backend/endpoints/api/v1/team-member-profiles.test.ts
  • apps/backend/src/app/api/latest/team-invitations/accept/verification-code-handler.tsx
  • apps/backend/src/app/api/latest/team-member-profiles/crud.tsx
apps/backend/src/app/api/latest/**

📄 CodeRabbit inference engine (CLAUDE.md)

Place backend API route handlers under /apps/backend/src/app/api/latest and follow RESTful, resource-based paths (auth, users, teams, oauth-providers)

Files:

  • apps/backend/src/app/api/latest/team-invitations/accept/verification-code-handler.tsx
  • apps/backend/src/app/api/latest/team-member-profiles/crud.tsx
🧬 Code graph analysis (3)
apps/e2e/tests/backend/endpoints/api/v1/team-member-profiles.test.ts (1)
apps/e2e/tests/backend/backend-helpers.ts (1)
  • niceBackendFetch (107-166)
apps/backend/src/app/api/latest/team-invitations/accept/verification-code-handler.tsx (4)
packages/stack-shared/src/schema-fields.ts (2)
  • yupArray (213-216)
  • permissionDefinitionIdSchema (680-689)
apps/backend/src/prisma-client.tsx (1)
  • retryTransaction (124-193)
apps/backend/src/app/api/latest/team-memberships/crud.tsx (1)
  • teamMembershipsCrudHandlers (43-160)
apps/backend/src/lib/permissions.tsx (1)
  • grantTeamPermission (96-138)
apps/backend/src/app/api/latest/team-member-profiles/crud.tsx (1)
apps/backend/src/app/api/latest/users/crud.tsx (2)
  • getUsersLastActiveAtMillis (199-223)
  • getUserLastActiveAtMillis (188-194)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Security Check
🔇 Additional comments (1)
apps/e2e/tests/backend/endpoints/api/v1/team-member-profiles.test.ts (1)

311-322: LGTM: new test covers permission_ids presence.

Good addition aligning with API changes.

Comment thread apps/backend/src/app/api/latest/team-member-profiles/crud.tsx
Comment thread apps/backend/src/app/api/latest/team-member-profiles/crud.tsx
- Changed the return statement in the `get` method to use `await res.json()` instead of `res.data` for proper JSON handling.

This change ensures that the API response is correctly parsed as JSON, improving data handling in the client interface.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts (1)

1504-1523: Return camelCase from getTeamRolePermissions and map the payload

Leaking snake_case (contained_permission_ids, is_paginated) from HTTP breaks JS client conventions and prior guidance. Map to containedPermissionIds and isPaginated.

-  async getTeamRolePermissions(): Promise<{ items: { id: string, description?: string, contained_permission_ids: string[] }[], is_paginated: false }> {
+  async getTeamRolePermissions(): Promise<{ items: { id: string; description?: string; containedPermissionIds: string[] }[]; isPaginated: false }> {
     const session = await this._getSession();
     const result = await this._interface.sendClientRequest(
       "/team-invitations/role-permissions",
       {
         method: "GET",
         headers: {
-          "Content-Type": "application/json",
+          "Accept": "application/json",
         },
       },
       session
     );
 
     if (!result.ok) {
       throw new Error(`Failed to get team role permissions: ${result.status} ${await result.text()}`);
     }
 
-    const data = await result.json();
-    return data;
+    const json = await result.json() as { items: { id: string; description?: string; contained_permission_ids: string[] }[]; is_paginated: false };
+    return {
+      items: json.items.map(({ id, description, contained_permission_ids }) => ({
+        id,
+        description,
+        containedPermissionIds: contained_permission_ids,
+      })),
+      isPaginated: false,
+    };
   }
🧹 Nitpick comments (3)
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts (3)

657-665: Default permissionIds to an empty array to avoid undefined at call sites

Safer for consumers and UI mapping.

       teamProfile: {
         displayName: crud.display_name,
         profileImageUrl: crud.profile_image_url,
-        permissionIds: crud.permission_ids,
+        permissionIds: crud.permission_ids ?? [],
       }

668-676: Also default invitation.permissionIds to []

Keeps the client shape stable even when backend omits the field.

       expiresAt: new Date(crud.expires_at_millis),
-      permissionIds: crud.permission_ids,
+      permissionIds: crud.permission_ids ?? [],

1511-1513: Use Accept for GET; Content-Type is unnecessary without a body

Minor HTTP header nit.

-        headers: {
-          "Content-Type": "application/json",
-        },
+        headers: { "Accept": "application/json" },
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6940ffe and 631b831.

📒 Files selected for processing (4)
  • packages/stack-shared/src/interface/client-interface.ts (2 hunks)
  • packages/stack-shared/src/interface/server-interface.ts (2 hunks)
  • packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts (4 hunks)
  • packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/stack-shared/src/interface/server-interface.ts
  • packages/stack-shared/src/interface/client-interface.ts
  • packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts
🧰 Additional context used
📓 Path-based instructions (2)
packages/template/**

📄 CodeRabbit inference engine (AGENTS.md)

When modifying the SDK copies, make changes in packages/template (source of truth)

Files:

  • packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Prefer ES6 Map over Record when representing key–value collections

Files:

  • packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
🧬 Code graph analysis (1)
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts (1)
packages/template/src/utils/url.ts (1)
  • constructRedirectUrl (4-20)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Security Check
🔇 Additional comments (1)
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts (1)

740-747: No action — sendTeamInvitation already maps permissionIds → permission_ids

packages/stack-shared/src/interface/client-interface.ts defines sendTeamInvitation(options: { permissionIds?: string[] }) and sends permission_ids: options.permissionIds; server-interface does the same. No change required.

@BilalG1
Copy link
Copy Markdown
Collaborator

BilalG1 commented Nov 17, 2025

Hi @bootssecurity, we're looking to merge this pr. Can you make these adjustments?

  • Merge latest dev branch in
  • Add tests to handle edge cases in new role-permissions route
  • Add tests to handle changes in existing routes (send-code route, team-member-profiles, team-invitations/accept)
  • Update client app. Remove getTeamRolePermissions and add getPermission / usePermission where we pass in a specific teamPermissionId and check whether user has given permission

…n_ids validation

- Added tests for fetching role permissions from the role-permissions endpoint.
- Implemented tests for sending invitations with specific permission_ids and verifying their application upon acceptance.
- Updated team member profile tests to ensure permission_ids are included in responses for both the inviting and invited users.
- Refactored permission fetching logic in the frontend to utilize the new project.listTeamPermissionDefinitions method.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
apps/e2e/tests/backend/endpoints/api/v1/team-invitations.test.ts (1)

559-579: Strengthen assertions and add edge case coverage.

The test validates basic response structure but could be more thorough:

  • The conditional check at line 573 means the test passes with zero permissions, leaving structure assertions unverified.
  • No validation of actual permission IDs or values returned.
  • Missing error scenarios (e.g., unauthenticated requests, server access type behavior).

Consider these improvements:

 it("can fetch role permissions from role-permissions endpoint", async ({ expect }) => {
   await Auth.Otp.signIn();
 
   const response = await niceBackendFetch("/api/v1/team-invitations/role-permissions", {
     accessType: "client",
     method: "GET",
   });
 
   expect(response.status).toBe(200);
   expect(response.body).toHaveProperty('items');
   expect(response.body).toHaveProperty('is_paginated', false);
   expect(Array.isArray(response.body.items)).toBe(true);
+  expect(response.body.items.length).toBeGreaterThan(0);
 
-  // Check that each item has the correct structure
-  if (response.body.items.length > 0) {
-    const item = response.body.items[0];
+  // Validate every item has the correct structure
+  for (const item of response.body.items) {
     expect(item).toHaveProperty('id');
+    expect(typeof item.id).toBe('string');
+    expect(item.id.length).toBeGreaterThan(0);
     expect(item).toHaveProperty('contained_permission_ids');
     expect(Array.isArray(item.contained_permission_ids)).toBe(true);
   }
 });

Additionally, consider adding tests for:

  • Unauthenticated access returning 401
  • Server access type behavior
  • Validation that known permission IDs (e.g., "team_admin") are present in the response
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 631b831 and 866cfaf.

📒 Files selected for processing (2)
  • apps/e2e/tests/backend/endpoints/api/v1/team-invitations.test.ts (1 hunks)
  • apps/e2e/tests/backend/endpoints/api/v1/team-member-profiles.test.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/e2e/tests/backend/endpoints/api/v1/team-member-profiles.test.ts
🧰 Additional context used
🧬 Code graph analysis (1)
apps/e2e/tests/backend/endpoints/api/v1/team-invitations.test.ts (3)
apps/e2e/tests/backend/backend-helpers.ts (3)
  • niceBackendFetch (109-173)
  • createMailbox (59-66)
  • backendContext (35-57)
packages/template/src/lib/stack-app/projects/index.ts (1)
  • Project (10-15)
packages/template/src/lib/stack-app/teams/index.ts (1)
  • Team (38-52)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Vercel Agent Review

Comment thread apps/e2e/tests/backend/endpoints/api/v1/team-invitations.test.ts
Comment thread apps/e2e/tests/backend/endpoints/api/v1/team-invitations.test.ts
Comment thread apps/e2e/tests/backend/endpoints/api/v1/team-invitations.test.ts
bootssecurity and others added 4 commits November 17, 2025 13:38
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
- Introduced tests for handling invalid, malformed, and mixed permission_ids in team invitations, ensuring proper error responses.
- Added tests for permission escalation scenarios, verifying that users without invite permissions cannot grant elevated permissions.
- Implemented tests for backwards compatibility with empty and omitted permission_ids.
- Included server access tests to confirm that invitations can be sent with permission_ids bypassing user permissions.
- Enhanced role-permissions endpoint tests for consistent responses between client and server access.
- Updated logic to ensure that only non-default roles are included in permissionIds when inviting users to a team, enhancing the accuracy of permission assignments during the invitation process.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (3)
packages/template/src/components-page/account-settings/teams/team-member-invitation-section.tsx (3)

40-42: Remove debug console.log statements.

These debug statements should be removed before merging to production.

Apply this diff:

     const fetchRolePermissions = async () => {
       try {
-        console.log('Fetching role permissions...');
         const permissions = await project.listTeamPermissionDefinitions();
-        console.log('Role permissions fetched:', permissions);
         setRolePermissions(permissions);
       } catch (error) {

144-144: Remove duplicate watch destructuring.

watchForm is destructured but never used in the code, and watch is already destructured earlier in the same statement.

Apply this diff:

-  const { register, handleSubmit, formState: { errors }, watch, setValue, watch: watchForm } = useForm({
+  const { register, handleSubmit, formState: { errors }, watch, setValue } = useForm({

211-225: Make Select controlled to enable visual reset.

The Select component is currently uncontrolled (missing value prop), so calling setValue('role', '') after form submission doesn't reset the visual selection. Users will see the previously selected role still displayed after inviting a member.

Apply this diff to make the Select controlled:

-              <Select onValueChange={(value) => setValue('role', value)}>
+              <Select value={watch('role') ?? 'default'} onValueChange={(value) => setValue('role', value)}>
                 <SelectTrigger>

And update the default value and reset logic:

     defaultValues: {
       email: '',
-      role: '',
+      role: 'default',
     }
       // Reset form
       setValue('email', '');
-      setValue('role', '');
+      setValue('role', 'default');
🧹 Nitpick comments (2)
packages/template/src/components-page/account-settings/teams/team-member-invitation-section.tsx (2)

65-86: Simplify getRoleDisplayName logic.

The current implementation creates a redundant Map that maps role.id to role.id, then performs lookups that simply return the original ID, followed by manual string comparisons for friendly names. This can be simplified significantly.

Apply this diff to streamline the logic:

   const getRoleDisplayName = (permissionIds: string[]) => {
-    if (permissionIds.length === 0) {
-      return t("Default member role");
-    }
-
     // Filter out permission IDs that start with $
     const filteredPermissionIds = permissionIds.filter(id => !id.startsWith('$'));
     if (filteredPermissionIds.length === 0) {
       return t("Default member role");
     }
 
-    // Map permission IDs to their IDs (instead of descriptions)
-    const roleMap = new Map(rolePermissions.map(role => [role.id, role.id]));
-
-
-    // Find the role that matches the permission IDs and return the ID
-    const matchingRoles = filteredPermissionIds.map(id => roleMap.get(id)).filter(Boolean);
-
-
-    if (matchingRoles.length > 0) {
-      const roleId = matchingRoles[0];
-      // Map common roles to friendly names
-      if (roleId === 'team_admin') return 'Admin';
-      if (roleId === 'team_member') return 'Member';
-      return roleId;
-    }
-
-    // Fallback to permission ID with mapping
-    const firstPermissionId = filteredPermissionIds[0];
-    if (firstPermissionId === 'team_admin') return 'Admin';
-    if (firstPermissionId === 'team_member') return 'Member';
-    return firstPermissionId;
+    // Map common roles to friendly names
+    const roleId = filteredPermissionIds[0];
+    if (roleId === 'team_admin') return 'Admin';
+    if (roleId === 'team_member') return 'Member';
+    return roleId;
   };

155-168: Consider extracting role permissions fetching to a custom hook.

The role permissions fetching logic is duplicated in two places (lines 37-52 and here). Extracting it to a custom hook like useTeamRolePermissions() would improve maintainability and reduce code duplication.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 866cfaf and 729f46e.

📒 Files selected for processing (2)
  • apps/e2e/tests/backend/endpoints/api/v1/team-invitations.test.ts (1 hunks)
  • packages/template/src/components-page/account-settings/teams/team-member-invitation-section.tsx (4 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-11T04:13:19.308Z
Learnt from: N2D4
Repo: stack-auth/stack-auth PR: 943
File: examples/convex/app/action/page.tsx:23-28
Timestamp: 2025-10-11T04:13:19.308Z
Learning: In the stack-auth codebase, use `runAsynchronouslyWithAlert` from `stackframe/stack-shared/dist/utils/promises` for async button click handlers and form submissions instead of manual try/catch blocks. This utility automatically handles errors and shows alerts to users.

Applied to files:

  • packages/template/src/components-page/account-settings/teams/team-member-invitation-section.tsx
🧬 Code graph analysis (2)
packages/template/src/components-page/account-settings/teams/team-member-invitation-section.tsx (2)
packages/template/src/lib/translations.tsx (1)
  • useTranslation (4-19)
packages/stack-shared/src/schema-fields.ts (3)
  • yupObject (247-251)
  • strictEmailSchema (448-448)
  • yupString (187-190)
apps/e2e/tests/backend/endpoints/api/v1/team-invitations.test.ts (3)
apps/e2e/tests/backend/backend-helpers.ts (3)
  • niceBackendFetch (109-173)
  • createMailbox (59-66)
  • backendContext (35-57)
packages/template/src/lib/stack-app/projects/index.ts (1)
  • Project (10-15)
packages/template/src/lib/stack-app/teams/index.ts (1)
  • Team (38-52)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Vercel Agent Review
🔇 Additional comments (2)
apps/e2e/tests/backend/endpoints/api/v1/team-invitations.test.ts (2)

559-579: LGTM: Role-permissions endpoint test is well-structured.

The test correctly validates the response structure and properties for the new role-permissions endpoint.


581-663: LGTM: Happy path tests for permission_ids are thorough.

Both tests correctly validate the full flow: sending invitations with permission_ids and verifying they are applied upon acceptance.

Comment thread apps/e2e/tests/backend/endpoints/api/v1/team-invitations.test.ts
Comment thread apps/e2e/tests/backend/endpoints/api/v1/team-invitations.test.ts
Comment thread apps/e2e/tests/backend/endpoints/api/v1/team-invitations.test.ts
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Comment thread apps/e2e/tests/backend/endpoints/api/v1/team-invitations.test.ts Outdated
bootssecurity and others added 3 commits November 17, 2025 14:33
- Updated test cases to use `receiveMailbox.emailAddress` for email field in team invitation requests.
- Simplified error response assertions by removing unnecessary nested property checks for 'error', focusing directly on 'code' and 'message' properties.
- Enhanced consistency in error handling across various invitation scenarios, ensuring clearer validation of permission-related errors.
Comment thread apps/backend/src/app/api/latest/team-member-profiles/crud.tsx
@BilalG1
Copy link
Copy Markdown
Collaborator

BilalG1 commented Nov 20, 2025

Moved here to fix tests:

#1022

@BilalG1 BilalG1 closed this Nov 20, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants